package com.AdvancedDemos.LearnThread多线程;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class 线程安全问题 {
    public static void main(String[] args) {
        /**
         * 什么是线程安全问题?
         * 多个线程，同时操作同一个共享资源的时候，可能会出现业务安全问题。
         *
         * 线程安全问题出现的原因?
         * 1.存在多个线程在同时执行
         * 2.同时访问一个共享资源
         * 3.存在修改该共享资源
         */

        /**
         * 取钱案例
         * 需求:
         * 小明和小红是一对夫妻，他们有一个共同的账户，余额是10万元，模拟2人同时去取钱10万。
         *
         * 分析:
         * ①:需要提供一个账户类，接着创建一个账户对象代表2个人的共享账户。
         * ②:需要定义一个线程类（用于创建两个线程，分别代表小明和小红)。
         * ③:创建2个线程，传入同一个账户对象给2个线程处理。
         * ④:启动2个线程，同时去同一个账户对象中取钱10万。
         */


        //1.创建一个账户对象,代表两个人的共享对象
        Account acc = new Account(100000, "ICBC-110");
        //2.创建两个线程分别代表小明和小红,再去同一个账户对象中取钱100000
        new Thread(new DrawMoneyThread(acc), "小明").start();//代表小明
        new Thread(new DrawMoneyThread(acc), "小红").start();//代表小红

        //小红来取钱100000.0成功!
        //小明来取钱100000.0成功!
        //小红来取钱后,余额剩余:0.0
        //小明来取钱后,余额剩余:-100000.0

        //小明来取钱100000.0成功!
        //小红来取钱100000.0成功!
        //小红来取钱后,余额剩余:-100000.0
        //小明来取钱后,余额剩余:0.0


        /**
         * 如何解决线程安全问题?
         * 线程同步,让多个线程先后依次访问共享资源,这样就解决了安全问题
         * 常用方案是加锁:
         * 方式一:同步代码块
         * 方式二:同步方法
         * 方式三:Lock锁
         */

        /*
        一、同步代码块:
        把访问共享资源的核心代码块给上锁,以此保证线程安全

        synchronized(同步锁对象){
            访问共享资源的核心代码
        }

        原理:
        每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

        同步锁的注意事项:
        对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出BUG

        synchronized (this) {//这里用共享的资源对象作为锁对象,既保证了同时执行该代码段的线程能够线程同步,又不至于使锁的范围过大影响其他无关线程的执行
            //1.判断余额是否足够
            if (balance >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                balance -= money;
                System.out.println(name + "来取钱后,余额剩余:" + balance);
            } else {
                System.out.println(name + "来取钱:余额不足");
            }
        }

        小明来取钱100000.0成功!
        小明来取钱后,余额剩余:0.0
        小红来取钱:余额不足

        静态方法通常使用字节码 类名.class 来作为同步锁对象,可以锁住使用类名访问当前方法的所有线程,每次只允许一个线程进来访问
        public static void test(){
            synchronized(Account.class){
                方法体
            }
        }

         */

        /*
        二、同步方法:
        把访问共享资源的核心方法给上锁(在方法定义时前面加上synchronized关键字)，以此保证线程安全。

        修饰符 synchronized 返回值类型 方法名称(形参列表){
            操作共享资源的代码
        }

        原理:每次只能一个线程进入，执行完毕以后自动解锁，其他线程才可以进来执行

        drawMoney在访问共享的Account对象,所以应当声明为synchronized,实例方法隐含使用this作为锁对象,静态方法隐含使用类名.class作为锁对象
        public synchronized void drawMoney(double money) {
            //哪个线程在取钱?
            String name = Thread.currentThread().getName();
            //1.判断余额是否足够
            if (balance >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                balance -= money;
                System.out.println(name + "来取钱后,余额剩余:" + balance);
            } else {
                System.out.println(name + "来取钱:余额不足");
            }
        }

        小明来取钱100000.0成功!
        小明来取钱后,余额剩余:0.0
        小红来取钱:余额不足
         */

        //范围上:同步代码块锁的范围更小,同步方法锁的范围更大。锁的范围越小性能越好,因为所有线程都可以先进入方法执行一些不涉及线程安全的代码,直到遇到让线程不安全的代码,才开始排队
        //但是对于现代计算机,这点差异忽略不计,可读性上同步方法更好

        /*
        三、Lock锁
        Lock锁是JDK5开始提供的一个新的锁定操作，通过它可以创建出锁对象进行加锁和解锁，更灵活、更方便、更强大。
        Lock是接口，不能直接实例化，可以采用它的实现类ReentrantLock来构建Lock锁对象。(reentrant [ˌriˈɛntrənt] a.可重入的,可再入的)

        //在Account中创建锁对象成员变量
        private Lock final lk = new ReentrantLock();//创建了一个锁对象(成员变量),每个Account对象都会有自己的锁对象,final修饰更专业,禁止替换锁对象
        然后与同步代码块类似,在线程不安全的区域开始前调用
        try{
            lk.lock();//加锁,多个线程执行到此处,将只允许一个线程加锁进入
            ...具体线程不安全的区域...
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //使用try-catch-finally使得某线程加锁后即便产生异常,停止执行后续代码,也必定会先执行unlock操作,不影响其他线程的执行
            lk.unlock();//解锁
        }

        小红来取钱100000.0成功!
        小红来取钱后,余额剩余:0.0
        小明来取钱:余额不足


         */

    }
}

class Account {
    private final Lock lk = new ReentrantLock();//创建了一个锁对象(成员变量),每个Account对象都会有自己的锁对象,final修饰更专业,禁止替换锁对象
    double balance;//余额
    String cardId;//卡号

    public Account() {

    }

    public Account(double balance, String cardId) {
        this.balance = balance;
        this.cardId = cardId;
    }

    public void drawMoney(double money) {
        //哪个线程在取钱?
        String name = Thread.currentThread().getName();

        try {
            lk.lock();//加锁,多个线程执行到此处,将只允许一个线程加锁进入
            //1.判断余额是否足够
            if (balance >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                balance -= money;
                System.out.println(name + "来取钱后,余额剩余:" + balance);
            } else {
                System.out.println(name + "来取钱:余额不足");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //使用try-catch-finally使得某线程加锁后即便产生异常,停止执行后续代码,也必定会先执行unlock操作,不影响其他线程的执行
            lk.unlock();//解锁
        }
    }
}

class DrawMoneyThread implements Runnable {
    Account acc;//共同账户对象

    public DrawMoneyThread() {
    }

    public DrawMoneyThread(Account acc) {
        this.acc = acc;
    }

    @Override
    public void run() {
        //取钱
        acc.drawMoney(100000);
    }
}